/**
 * Generate code from /{{projectName}}-core/src/main/java/{{packageName}}/core/{{modules}}/service/impl/{{entities@MINHERIT}}ServiceImpl.java.hbs
 */
// @SkipOverwrite
package cn.ibizlab.core.authentication.service.impl;

import cn.ibizlab.core.ad.domain.SysDepartment;
import cn.ibizlab.core.ad.domain.SysDomain;
import cn.ibizlab.core.ad.domain.SysOrganization;
import cn.ibizlab.core.ad.domain.SysPerson;
import cn.ibizlab.core.ad.service.SysDepartmentService;
import cn.ibizlab.core.ad.service.SysDomainService;
import cn.ibizlab.core.ad.service.SysOrganizationService;
import cn.ibizlab.core.authentication.config.DefaultDigitsCreator;
import cn.ibizlab.core.authentication.constants.AuthenticationConstant;
import cn.ibizlab.core.authentication.domain.AuthInfo;
import cn.ibizlab.core.authentication.domain.AuthLogin;
import cn.ibizlab.core.authentication.domain.SMSResponse;
import cn.ibizlab.core.authentication.enums.AuthenticationType;
import cn.ibizlab.core.authentication.mapping.AuthInfoMapping;
import cn.ibizlab.core.authentication.service.CaptchaService;
import cn.ibizlab.core.authentication.service.SMSService;
import cn.ibizlab.core.authentication.service.SocialService;
import cn.ibizlab.core.oauth2.config.OAuth2TokenAdapter;
import cn.ibizlab.core.open.domain.OpenAccess;
import cn.ibizlab.core.open.domain.OpenAccessAuth;
import cn.ibizlab.core.open.filter.OpenAccessAuthSearchContext;
import cn.ibizlab.core.open.filter.OpenAccessSearchContext;
import cn.ibizlab.core.open.service.OpenAccessAuthService;
import cn.ibizlab.core.open.service.impl.OpenAccessServiceImpl;
import cn.ibizlab.util.common.CacheStore;
import cn.ibizlab.util.enums.Entities;
import cn.ibizlab.util.errors.BadRequestAlertException;
import cn.ibizlab.util.security.AuthUser20Impl;
import cn.ibizlab.util.security.AuthenticationUser;
import cn.ibizlab.util.security.JwkStore;
import cn.ibizlab.util.web.IpPatternMatcher;
import cn.ibizlab.util.web.PasswordStrengthValidator;
import cn.ibizlab.util.web.Sm3Util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSADecrypter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

import static cn.ibizlab.core.authentication.constants.AuthenticationConstant.*;

/**
 * 用户验证实现类，包含用户登录、注销等处理。
 */
@Slf4j
@Service("AuthLoginService")
public class AuthLoginServiceImpl extends AbstractAuthLoginService{

    @Value("${ibiz.auth.passwordStorageMode:none}")
    private String passwordStorageMode;
    @Value("${ibiz.auth.passwordEncryptMode:none}")
    private String passwordEncryptMode;
    @Value("${ibiz.auth.passwordMinStrength:1}")
    private Integer passwordMinStrength;
    @Value("${ibiz.auth.apiUser.allowedIps:}")
    private String allowedIps;
    @Value("${ibiz.systemid}")
    private String systemId;
    @Value("${ibiz.auth.imageCaptchaExpiration:120000}")
    private Long imageCaptchaExpiration;
    @Value("${sms.verificationCodeLength:6}")
    private int smsVerificationCodeLength;
    @Value("${ibiz.auth.imageCaptcha.enable:false}")
    private boolean enableImageCaptcha;
    @Value("${sms.blends.yunpian.templateName:code}")
    private String templateName;

    private static final String ROLE_API_USERS = "ROLE_APIUSERS";
    @Autowired
    @Lazy
    private AuthenticationManager authenticationManager;
    @Autowired
    @Lazy
    private cn.ibizlab.util.security.AuthUserServiceImpl authUserService;
    @Autowired
    private AuthInfoMapping authUserMapping;
    @Autowired
    private JwkStore jwkStore;
    @Autowired
    private SysDomainService domainService;
    @Autowired
    @Lazy
    private SysOrganizationService organizationService;
    @Autowired
    @Lazy
    private SysDepartmentService departmentService;
    @Autowired
    private AuthenticationService authenticationService;
    @Autowired
    private OpenAccessServiceImpl openAccessService;
    @Autowired
    private OpenAccessAuthService openAccessAuthService;
    @Autowired
    private SMSService smsService;
    @Autowired
    private SocialService socialService;
    @Autowired(required = false)
    private CaptchaService captchaService;

    Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap<>();

    /**
     * 用户登录
     * @param authLogin 登录信息
     * @return 认证用户、访问令牌等
     */
    @Override
    public AuthLogin login(AuthLogin authLogin) {
        if(ObjectUtils.isEmpty(authLogin.getLoginName())||ObjectUtils.isEmpty(authLogin.getPassword()))
            throw new BadRequestAlertException("请输入登录名或密码","AuthUser",authLogin.getLoginName());
        String username = !ObjectUtils.isEmpty(authLogin.getDc())? String.format("%1$s|%2$s",authLogin.getLoginName(),authLogin.getDc()) : authLogin.getLoginName();
        ReentrantLock lock = keyLockMap.computeIfAbsent(username, k -> new ReentrantLock());
        lock.lock();

        try
        {
            //登录特殊处理(mobile、mixed)
            if (!ObjectUtils.isEmpty(authLogin.getLoginType()))
                username = resolveUsername(authLogin);

            if(authLogin.getOrDefault("focus_refresh_token",false))
                authUserService.revokeToken(username,600);
            else
                authUserService.resetByUsername(username);

            String password = resolvePassword(authLogin.getPassword(),authLogin.getState());
            //用户认证
            Authentication usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username,password);
            Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
            if(authentication == null || authentication.getPrincipal()== null || !(authentication.getPrincipal() instanceof UserDetails)){
                throw new BadRequestAlertException("用户名或密码错误","AuthUser",authLogin.getLoginName());
            }
            //生成认证用户登录凭证
            AuthUser20Impl authUser = (AuthUser20Impl) authentication.getPrincipal();
            authUser.setPassword(null);
            authUser.readHeader(true);

            //构造认证用户
            AuthInfo authInfo = authUserMapping.toAuthInfo(authUser);
            if(!ObjectUtils.isEmpty(authInfo.getGrants())) {
                authInfo.getGrants().forEach(sys -> {
                    if (!ObjectUtils.isEmpty(sys.getRoles()))
                        sys.getRoles().forEach(role -> {
                            if (ROLE_API_USERS.equals(role.getAuthority())
                                    && !ObjectUtils.isEmpty(allowedIps) && !IpPatternMatcher.getInstance(allowedIps).isIpAllowed(null))
                                throw new BadRequestAlertException("API用户登录请求地址未授权","AuthUser",authLogin.getLoginName());
                            role.setAuthorities(null);
                        });
                });
            }

            //构造认证用户信息及登录凭证
            AuthLogin authLogin2 = generateLoginCredentials(authUser);
            authLogin2.setLoginName(authLogin.getLoginName());
            String defaultDC = getDefaultDC();
            authLogin2.setDc(!ObjectUtils.isEmpty(authLogin.getDc())? authLogin.getDc() : defaultDC);
            authLogin2.setUser(authInfo);
            authInfo.setDc(!ObjectUtils.isEmpty(authInfo.getDc())? authInfo.getDc() : defaultDC);
            return authLogin2;
        }
        finally {
            lock.unlock();
        }
    }


    public AuthLogin loadUserAndTokenByUsername(String username, boolean focus_refresh_token) {
        AuthenticationUser user = authUserService.loadUserByUsername(username);
        AuthLogin authLogin2 = null;
        if (user != null && ObjectUtils.isEmpty(user.getToken()) )
        {

            if(focus_refresh_token)
                authUserService.revokeToken(username,600);
            AuthInfo authInfo = authUserMapping.toAuthInfo((AuthUser20Impl)user);
            if(!ObjectUtils.isEmpty(authInfo.getGrants())) {
                authInfo.getGrants().forEach(sys -> {
                    if (!ObjectUtils.isEmpty(sys.getRoles()))
                        sys.getRoles().forEach(role -> role.setAuthorities(null));
                });
            }

            //构造认证用户信息及登录凭证
            authLogin2 = generateLoginCredentials(user);
            authLogin2.setLoginName(user.getUsername());
            String defaultDC = getDefaultDC();

            authLogin2.setUser(authInfo);
            authInfo.setDc(!ObjectUtils.isEmpty(authInfo.getDc())? authInfo.getDc() : defaultDC);
        }
        return authLogin2;
    }

    /**
     * 通过认证用户生成token
     * @param authUser
     * @return
     */
    protected AuthLogin generateLoginCredentials(AuthenticationUser authUser) {
        AuthLogin authLogin = new AuthLogin();
        Object token = authUserService.generateAccessToken(authUser);
        if (token instanceof OAuth2AccessToken) {
            OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
            authLogin.setTokenType(accessToken.getTokenType());
            authLogin.setTokenId((String) accessToken.getAdditionalInformation().get("token_id"));
            if (!ObjectUtils.isEmpty(accessToken.getScope()))
                authLogin.setScope(String.join(" ", accessToken.getScope()));
            authLogin.setToken(accessToken.getValue());
            authLogin.setAccessToken(accessToken.getValue());
            authLogin.setExpiration(accessToken.getExpiration().getTime());
            authLogin.setExpiresIn(accessToken.getExpiresIn());
            authLogin.setRefreshToken(accessToken.getRefreshToken().getValue());
        } else {
            authLogin.setTokenType("bearer");
            authLogin.setToken((String) token);
            if (authUser.getExpiration() != null) {
                authLogin.setExpiration(authUser.getExpiration().getTime());
                authLogin.setExpiresIn((int) (authUser.getExpiration().getTime() - System.currentTimeMillis()));
            }
        }
        return authLogin;
    }

    @Cacheable(value = "sysorganization", key = "'orginfo:'+#p0+':'+#p1")
    public Map<String,List<String>> getOrgInfo(String orgId, String deptId) {
        Map<String,List<String>> orgInfo = new LinkedHashMap<>();
        if(!ObjectUtils.isEmpty(orgId)) {
            SysOrganization organization = new SysOrganization().setId(orgId);
            orgInfo.put("organization_parent_ids", organizationService.parentIds(organization));
            orgInfo.put("organization_sub_ids", organizationService.subIds(organization));
            if(!ObjectUtils.isEmpty(deptId) && !deptId.equalsIgnoreCase("null")) {
                SysDepartment department = new SysDepartment().setId(deptId).setOrganizationId(orgId);
                orgInfo.put("department_parent_ids", departmentService.parentIds(department));
                orgInfo.put("department_sub_ids", departmentService.subIds(department));
            }
        }
        return orgInfo;
    }

    /**
     * 解析密码
     * @param password
     * @param state
     * @return
     */
    protected String resolvePassword(String password, String state) {
        //解析rsa格式密码
        if(PASSWORD_ENCRYPT_MODE_STATE.equals(passwordEncryptMode)|| "RSA".equalsIgnoreCase(state)){
            //获取密码
            try
            {
                if (!ObjectUtils.isEmpty(password)) {
                    password = decryptWithRSA(password);
                    //还原真实密码
                    if (password.split("[|]").length > 1) {
                        password = password.split("[|]")[0];
                    }
                }
            }catch (Exception ex) {

            }
        }
        return password;
    }

    /**
     * 手机登录: 通过手机号查询登录用户名
     * @param authLogin
     * @return
     */
    protected String resolveUsername(AuthLogin authLogin) {

        String username = authLogin.getLoginName();
        if (!ObjectUtils.isEmpty(username)) {
            if (LOGIN_TYPE_MOBILE.equals(authLogin.getLoginType())) {
                SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().select(SysPerson::getUid).eq(SysPerson::getMobile, username).or().eq(SysPerson::getTelephoneNumber, username), false);
                if (person != null && !ObjectUtils.isEmpty(person.getUid()))
                    return person.getUid();
            } else if (LOGIN_TYPE_MIXED.equals(authLogin.getLoginType())) {
                SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().select(SysPerson::getUid).eq(SysPerson::getMobile, username)
                        .or().eq(SysPerson::getMobile, username)
                        .or().eq(SysPerson::getTelephoneNumber, username)
                        .or().eq(SysPerson::getUid, username), false);
                if (person != null && !ObjectUtils.isEmpty(person.getUid()))
                    return person.getUid();
            }
        }
        return username;
    }

    /**
     * 解析rsa格式数据
     * @param data
     * @return
     */
    protected String decryptWithRSA(String data) {
        try {
            String[] parts = data.split("\\.", -1);
            if (parts.length == 5) {
                JWEObject jweObject = JWEObject.parse(data);
                // 使用私钥解密JWE对象
                jweObject.decrypt(jwkStore.getRSADecrypter());
                // 获取解密后的负载
                return jweObject.getPayload().toString();
            }
            else{
                Cipher cipher = jwkStore.getRsaCipher();
                if(cipher == null)
                    throw new BadRequestAlertException("获取rsa解析器失败","","");

                byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
                return new String(bytes);
            }
        } catch (Exception e) {
            throw new BadRequestAlertException(String.format("获取rsa数据发生异常,%1$s",e.getMessage()),"","");
        }
    }

    public String getSystemId() {
        return this.systemId;
    }

    /**
     * 用户解锁
     * @param username
     * @return
     */
    @Override
    public AuthLogin unlock(String username) {
        authenticationService.resetAuthUserLogin(username);
        return super.unlock(username);
    }

    /**
     * 用户注销
     * @param username
     * @return
     */
    @Override
    public AuthLogin logout(String username) {
        authUserService.logout(username);
        return super.logout(username);
    }

    @Override
    public AuthLogin refreshToken(String key) {
        Object token = authUserService.refreshAccessToken(key);
        if (token instanceof OAuth2AccessToken) {
            AuthLogin authLogin2 = new AuthLogin();
            OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
            authLogin2.setTokenType(accessToken.getTokenType());
            authLogin2.setTokenId((String)accessToken.getAdditionalInformation().get("token_id"));
            if(!ObjectUtils.isEmpty(accessToken.getScope()))
                authLogin2.setScope(String.join(" ",accessToken.getScope()));
            authLogin2.setToken(accessToken.getValue());
            authLogin2.setAccessToken(accessToken.getValue());
            authLogin2.setExpiration(accessToken.getExpiration().getTime());
            authLogin2.setExpiresIn(accessToken.getExpiresIn());
            authLogin2.setRefreshToken(accessToken.getRefreshToken().getValue());
            return authLogin2;
        }
        return super.refreshToken(key);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = "sysdeploysystem", allEntries = true),
            @CacheEvict(value = "sysopenaccess", allEntries = true),
            @CacheEvict(value = "sysrole", allEntries = true),
            @CacheEvict(value = "oauthclientdetails", allEntries = true),
            @CacheEvict(value = "authprovider", allEntries = true),
            @CacheEvict(value = "sysorganization", allEntries = true),
            @CacheEvict(value = "sysperson", allEntries = true),
            @CacheEvict(value = "authuser", allEntries = true)
    })
    public AuthLogin cacheEvict(AuthLogin dto) {
        CacheStore.getInstance().clearL2ByPattern("ibiz-cloud-clouddata-*--Employee");
        CacheStore.getInstance().clearL2ByPattern("ibiz-cloud-clouddata-*--OpenAccess");
        CacheStore.getInstance().clearL2ByPattern("ibiz-cloud-clouddata-*--OpenUser");
        return super.cacheEvict(dto);
    }

    private String defaultDC;
    public String getDefaultDC() {
        if (!ObjectUtils.isEmpty(defaultDC))
            return defaultDC;
        List<SysDomain> domains = domainService.list(Wrappers.<SysDomain>lambdaQuery().select(SysDomain::getId).orderByAsc(SysDomain::getCreateTime));
        if(!ObjectUtils.isEmpty(domains)) {
            defaultDC = domains.get(0).getId();
        }
        return defaultDC;
    }

    /**
     * 修改用户密码
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin changePwd(AuthLogin authLogin) {
        String postOldPassword = resolvePassword(authLogin.getPassword(),authLogin.getState());
        String postNewPassword = resolvePassword(authLogin.getNewPassword(),authLogin.getState());
        authLogin.setPassword(postOldPassword);
        authLogin.setNewPassword(postNewPassword);


        String newPassWord = postNewPassword;
        String Strength = PasswordStrengthValidator.validPassword(newPassWord, passwordMinStrength);
        if(!ObjectUtils.isEmpty(Strength))
            throw new BadRequestAlertException(Strength, Entities.AUTH_LOGIN.toString(),"The minimum password strength is not met");


        SysPerson person = null;
        //1.通过手机号或原始密码进行身份校验
        if (!ObjectUtils.isEmpty(authLogin.getPhone()))
            person = getUserByPhone(authLogin);
        else
            person = getUserById(authLogin);

        if (person == null)
            throw new BadRequestAlertException("用户不存在", Entities.AUTH_LOGIN.toString(), "User does not exist");

        //2.修改用户密码
        if(AuthenticationConstant.PASSWORD_STORAGE_MODE_SM3.equals(passwordStorageMode))
            newPassWord = Sm3Util.encrypt(newPassWord);

        person.setUserPassword(newPassWord);
        sysPersonService.update(person);

        //3.将密码同步到外部认证服务(如：ldap)
        authenticationService.syncChangePwd(person,authLogin.getPassword(),postNewPassword);

        authLogin.setNewPassword(null);
        authLogin.setPassword(null);

        return authLogin;
    }


    /**
     * 进行短信验证码校验，通过手机号获取用户信息
     * @param authLogin
     * @return
     */
    private SysPerson  getUserByPhone(AuthLogin authLogin){
        String cacheSMSCode = null;
        String phone = authLogin.getPhone();
        String smsCode = authLogin.getCode();

        if (ObjectUtils.isEmpty(smsCode))
            throw new BadRequestAlertException("验证码不允许为空", Entities.AUTH_LOGIN.toString(), "The verification code cannot be empty");

        cacheSMSCode = authenticationService.getSMSCode(phone);
        if (ObjectUtils.isEmpty(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码已失效", Entities.AUTH_LOGIN.toString(), "SMS code has expired");

        if (!smsCode.equals(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码不匹配，请重新输入", Entities.AUTH_LOGIN.toString(), "SMS code mismatch");

        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getMobile, phone));
        return person;

    }

    /**
     * 获取当前登录用户信息，进行原始密码校验
     * @param authLogin
     * @return
     */
    private SysPerson getUserById(AuthLogin authLogin){
        String oldPassword = authLogin.getPassword();

        if(ObjectUtils.isEmpty(oldPassword))
            throw new BadRequestAlertException("原始密码不允许为空", Entities.AUTH_LOGIN.toString(),"The original password cannot be empty");

        if(oldPassword.equals(authLogin.getNewPassword()))
            throw new BadRequestAlertException("新密码不能等于原始密码", Entities.AUTH_LOGIN.toString(),"New password cannot equal original password");

        AuthenticationUser curUser = AuthenticationUser.getAuthenticationUser();
        String userId = curUser.getUserid();
        if(ObjectUtils.isEmpty(userId)){
            throw new BadRequestAlertException("未知的当前用户身份", Entities.AUTH_LOGIN.toString(),"Unknown current user identity");
        }

        //校验用户旧密码是否正确（将用户输入密码与数据库中密码进行比较）
        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getId,userId));

        String userPassword = person.getUserPassword();
        if(AuthenticationConstant.PASSWORD_STORAGE_MODE_SM3.equals(passwordStorageMode)) {
            if (oldPassword.length() != 64)
                oldPassword = Sm3Util.encrypt(oldPassword);
            if (userPassword.length() != 64)
                userPassword = Sm3Util.encrypt(userPassword);
        }

        if(!oldPassword.equals(userPassword))
            throw new BadRequestAlertException("原始密码不正确", Entities.AUTH_LOGIN.toString(),"The original password is incorrect");

        return person;
    }


    /**
     * 发送短信验证码
     * 1. 验证图片验证码是否有效、是否已过期
     * 2. 生成短信验证码，调用短信服务，并发送验证码
     * 3. 缓存短信验证码
     * @param authLogin
     * @return
     */
    @Override
    public AuthLogin sendSmsCode(AuthLogin authLogin) {

        //发送短信前需校验验证码
        if(!enableImageCaptcha)
            throw new BadRequestAlertException("未开启图片验证码配置",Entities.AUTH_LOGIN.toString(),"ImageCaptcha configuration not enabled");

        //生成短信验证码
        if (smsVerificationCodeLength<=0)
            throw new BadRequestAlertException(String.format("短信验证码长度[%1$s]设置有误",smsVerificationCodeLength), Entities.AUTH_LOGIN.toString(), "Incorrect length setting for verification code");

        String phone = authLogin.getPhone();
        if (ObjectUtils.isEmpty(phone))
            throw new BadRequestAlertException("未传入手机号", Entities.AUTH_LOGIN.toString(), "No phone number passed in");

        //生成短信验证码
        String smsCode = SmsUtils.getRandomInt(smsVerificationCodeLength);

        log.debug("正在向手机[{}]发送短信验证码[{}]", phone, smsCode);

        //调用短信服务，发送验证码
        String templateId = authLogin.getId();
        SMSResponse result = new SMSResponse();
        //若templateId不为空，则使用指定模板发送短信，否则使用默认
        if (!ObjectUtils.isEmpty(templateId)) {
            LinkedHashMap<String, String> messages = new LinkedHashMap();
            messages.put(templateName, smsCode);
            if (!ObjectUtils.isEmpty(authLogin.get("params"))) {
                messages.putAll((LinkedHashMap) authLogin.get("params"));
            }
            result = smsService.sendMessage(phone, templateId, messages);
        } else
            result = smsService.sendMessage(phone, smsCode);

        if (!result.isSuccess())
            throw new BadRequestAlertException(String.format("发送短信失败,原因：%1$s", result.getMessage()), Entities.AUTH_LOGIN.toString(), "Send message error");

        //缓存短信验证码
        authenticationService.putSMSCode(phone, smsCode);

        return authLogin;
    }

    /**
     * 用户注册
     * 1. 用户名是否存在、手机号是否存在
     * 2. 短信验证码是否正确
     * 3. 创建用户
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin register(AuthLogin authLogin) {

        String cacheSMSCode = null;
        String phone = authLogin.getPhone();
        String smsCode = authLogin.getCode();
        String loginName = authLogin.getLoginName();
        String displayName = !ObjectUtils.isEmpty(authLogin.getDisplayName())? authLogin.getDisplayName() : loginName;

        if (ObjectUtils.isEmpty(loginName))
            throw new BadRequestAlertException("未传入用户名", Entities.AUTH_LOGIN.toString(), "No username passed in");

        if(!ObjectUtils.isEmpty(phone)){

            if (ObjectUtils.isEmpty(smsCode))
                throw new BadRequestAlertException("未传入短信验证码", Entities.AUTH_LOGIN.toString(), "No SMS code passed in");

            cacheSMSCode = authenticationService.getSMSCode(phone);
            if (ObjectUtils.isEmpty(cacheSMSCode))
                throw new BadRequestAlertException("短信验证码已失效", Entities.AUTH_LOGIN.toString(), "SMS code has expired");

            if (!smsCode.equals(cacheSMSCode))
                throw new BadRequestAlertException("短信验证码不匹配，请重新输入", Entities.AUTH_LOGIN.toString(), "SMS code mismatch");
        }

        //验证用户是否存在
        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getUid, loginName).or().eq(SysPerson::getMobile, phone), false);
        if (person != null)
            throw new BadRequestAlertException(String.format("%1$s已存在", loginName.equals(person.getUid()) ? "用户名" : "手机号"), Entities.AUTH_LOGIN.toString(), "User already exists");

        //获取默认单位
        SysOrganization defaultOrganization = getDefaultOrganization();

        //创建人员
        String employeeNumber = authLogin.get(REGISTER_EMPLOYEE_NUMBER) != null ? authLogin.get(REGISTER_EMPLOYEE_NUMBER).toString() : getDefaultEmployeeNumber();
        SysPerson sysPerson = new SysPerson().setOrganizationId(defaultOrganization.getId()).setOrganizationName(defaultOrganization.getOrganizationName()).setUid(loginName).setDisplayName(displayName).setMobile(phone).setUserPassword(authLogin.getPassword()).setEmployeeNumber(employeeNumber).setEmployeeType(AuthenticationType.SMS.name().toLowerCase());
        sysPersonService.create(sysPerson);
        //构造注册用户信息
        authLogin.setUser(authUserMapping.toAuthInfo(authUserMapping.toAuthUser(sysPerson)));
        //注册成功后，清除缓存中的验证码
        if (!ObjectUtils.isEmpty(cacheSMSCode) && !ObjectUtils.isEmpty(phone))
            authenticationService.resetSMSCode(phone);

        return authLogin;
    }

    /**
     * 社交平台用户注册：社交平台认证成功后，根据社交平台中的用户信息自动注册系统用户
     * 1. 获取社交平台用户信息
     * 2. 通过社交平台用户创建系统用户
     * 3. 将社交用户与系统用户进行绑定
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin socialRegister(AuthLogin authLogin) {

        String socialUserId = authLogin.getId();
        if (ObjectUtils.isEmpty(socialUserId))
            throw new BadRequestAlertException("未传入社交平台用户标识", Entities.AUTH_LOGIN.toString(), "SocialUserId not found");

        //查询社交用户信息
        OpenAccessAuth socialUser = authenticationService.getSocialUser(socialUserId);
        if (socialUser == null)
            throw new BadRequestAlertException("授权信息已失效，请重新授权", Entities.AUTH_LOGIN.toString(), "Authorization information has expired");

        //查询当前社交账户是否已经注册系统用户
        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getId, socialUser.getCredential()).or().eq(SysPerson::getUid, socialUser.getCredential()), false);
        if (person != null)
            throw new BadRequestAlertException("用户已存在", Entities.AUTH_LOGIN.toString(), "User already exists");

        //查询社交账户是否已经绑定过其他的系统用户
        Long socialUserBindCount = openAccessAuthService.count(Wrappers.<OpenAccessAuth>lambdaQuery().eq(OpenAccessAuth::getAccessId, socialUser.getAccessId()).eq(OpenAccessAuth::getCredential, socialUser.getCredential()));
        if (socialUserBindCount > 0)
            throw new BadRequestAlertException(String.format("社交用户[%1$s]存在绑定记录，无法重复绑定", socialUser.getName()), Entities.AUTH_LOGIN.toString(), "User already bound");

        //获取默认单位
        SysOrganization defaultOrganization = getDefaultOrganization();

        //创建人员
        String employeeType = socialUser.get(PROVIDER_SOCIAL_TYPE) != null ? socialUser.get(PROVIDER_SOCIAL_TYPE).toString() : null;
        String employeeNumber = authLogin.get(REGISTER_EMPLOYEE_NUMBER) != null ? authLogin.get(REGISTER_EMPLOYEE_NUMBER).toString() : getDefaultEmployeeNumber();
        SysPerson sysPerson = new SysPerson().setOrganizationId(defaultOrganization.getId()).setOrganizationName(defaultOrganization.getOrganizationName()).setUid(socialUser.getCredential()).setDisplayName(socialUser.getName()).setEmployeeNumber(employeeNumber).setEmployeeType(employeeType);
        sysPersonService.create(sysPerson);
        //将社交用户与系统用户进行绑定
        socialUser.setPersonId(sysPerson.getId()).setUid(sysPerson.getUid());
        bindSocialUser(socialUser);
        //构造注册用户信息
        AuthUser20Impl authenticationUser = authUserMapping.toAuthUser20Impl(sysPerson);
        //生成token
        authLogin = generateLoginCredentials(authenticationUser);
        //设置认证用户信息
        authLogin.setUser(authUserMapping.toAuthInfo(authenticationUser));
        //清除缓存
        authenticationService.resetSocialUser(socialUserId);
        return authLogin;

    }

    /**
     * 获取默认的人员编号
     * @return
     */
    protected String getDefaultEmployeeNumber() {
        return IdWorker.getIdStr();
    }

    /**
     * 通过社交平台授权码登录系统
     * 1. 通过授权码到社交平台中查询授权用户信息。
     * 2. 先通过社交平台账号查看已绑定。若已绑定，则通过绑定表拿到 personId, 并通过该person 生成token；若未绑定，则使用社交平台用户中的手机号去查询 person 表，查到则自动绑定，并生成token。
     * 3. 若无绑定记录，也无法关联person，则认为是新用户，则创建person，并创建绑定记录。
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin loginByCode(AuthLogin authLogin) {

        String code = authLogin.getCode();
        if (ObjectUtils.isEmpty(code))
            throw new BadRequestAlertException("未传入社交平台授权码", Entities.AUTH_LOGIN.toString(), "No social platform authorization code");

        String openAccessId = authLogin.getId();
        if (ObjectUtils.isEmpty(openAccessId))
            throw new BadRequestAlertException("未传入社交平台标识", Entities.AUTH_LOGIN.toString(), "No social platform id");

        //获取社交平台认证配置
        OpenAccess socialConfig = openAccessService.getById(openAccessId);
        if (socialConfig == null || 1==socialConfig.getDisabled())
            throw new BadRequestAlertException(String.format("未找到社交平台[%1$s]认证配置", openAccessId), Entities.AUTH_LOGIN.toString(), "OpenAccess not found");

        OpenAccessAuth socialUser = socialService.callback(socialConfig,authLogin.getCode(),authLogin.getState());
        String socialUserId = socialUser.getCredential();
        String socialUserMobile = socialUser.getMobile();
        SysPerson person = null;

        //查询社交平台用户是否已绑定
        OpenAccessAuth accessAuth = openAccessAuthService.getOne(Wrappers.<OpenAccessAuth>lambdaQuery().eq(OpenAccessAuth::getAccessId,openAccessId).eq(OpenAccessAuth:: getCredential,socialUserId));
        if(accessAuth == null){
            try {
                List<OpenAccessAuth> socialUserList = openAccessAuthService.searchCompatible(new OpenAccessAuthSearchContext().setAccessIdEQ(openAccessId).setPersonIdEQ(socialUserId)).getContent();
                if(!ObjectUtils.isEmpty(socialUserList))
                    accessAuth = socialUserList.get(0);
                if(accessAuth == null){
                    socialUserList = openAccessAuthService.searchCompatible(new OpenAccessAuthSearchContext().setPersonIdEQ(socialUserId)).getContent();
                    if(!ObjectUtils.isEmpty(socialUserList))
                        accessAuth = socialUserList.get(0);
                }
            }
            catch (Exception ex) {

            }

        }
        //通过绑定表查询person
        if(accessAuth != null && !ObjectUtils.isEmpty(accessAuth.getPersonId())){
            person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getId, accessAuth.getPersonId()));
        }

        //未匹配，再尝试使用电话匹配person
        if(person == null && !ObjectUtils.isEmpty(socialUserId)){
            person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getUid, socialUserId));
        }

        //未匹配，再尝试使用电话匹配person
        if(person == null && !ObjectUtils.isEmpty(socialUserMobile)){
            person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getMobile, socialUserMobile));
        }

        JSONObject userInfo = JSONObject.parseObject(ObjectUtils.isEmpty(socialUser.getParams())?"{}":socialUser.getParams());
        String employeeNumber = (String)userInfo.getOrDefault("employee_number",getDefaultEmployeeNumber());
        String employeeType = (String)userInfo.getOrDefault("employee_type",socialConfig.getOpenType() != null ?socialConfig.getOpenType().toLowerCase(): null);
        String openId = (String)userInfo.getOrDefault("open_id",userInfo.getString("identification_number"));
        String address = (String)userInfo.getOrDefault("postal_address",userInfo.getString("address"));

        String organizationId = userInfo.getString("organization_id");
        String organizationNumber = userInfo.getString("organization_number");
        String organizationName = userInfo.getString("organization_name");
        String departmentId = (String)userInfo.getOrDefault("mdepartment_id",userInfo.getString("department_id"));
        String departmentName = (String)userInfo.getOrDefault("mdepartment_name",userInfo.getString("department_name"));
        String organizationalRoleId = userInfo.getString("organizational_role_id");


        //不存在，创建person
        if(person == null){

            if(ObjectUtils.isEmpty(organizationId)) {
                SysOrganization defaultOrganization = getDefaultOrganization();
                organizationId = defaultOrganization.getId();
                organizationNumber = defaultOrganization.getOrganizationNumber();
                organizationName = defaultOrganization.getOrganizationName();
            }

            person = new SysPerson().setOrganizationId(organizationId)
                    .setOrganizationNumber(organizationNumber)
                    .setOrganizationName(organizationName)
                    .setUid(socialUserId)
                    .setDisplayName(socialUser.getName())
                    .setMobile(socialUserMobile)
                    .setAvatar(socialUser.getAvatar())
                    .setMail(socialUser.getMail())
                    .setEmployeeNumber(employeeNumber)
                    .setEmployeeType(employeeType)
                    .setMdepartmentId(departmentId)
                    .setMdepartmentName(departmentName)
                    .setOrganizationalRoleId(organizationalRoleId)
                    .setPostalAddress(address)
                    .setIdentificationNumber(openId);

            sysPersonService.create(person);
        }
        else {
            boolean needUpdate = false;
            if(organizationId!=null && !organizationId.equals(person.getOrganizationId())) {
                needUpdate = true;
                person.setOrganizationId(organizationId);
            }
            if(socialUserMobile!=null && !socialUserMobile.equals(person.getMobile())) {
                needUpdate = true;
                person.setMobile(socialUserMobile);
            }
            if(socialUser.getMail()!=null && !socialUser.getMail().equals(person.getMail())) {
                needUpdate = true;
                person.setMail(socialUser.getMail());
            }
            if(socialUser.getAvatar()!=null && !socialUser.getAvatar().equals(person.getAvatar())) {
                needUpdate = true;
                person.setAvatar(socialUser.getAvatar());
            }
            if(employeeNumber!=null && !employeeNumber.equals(person.getEmployeeNumber())) {
                needUpdate = true;
                person.setEmployeeNumber(employeeNumber);
            }
            if(employeeType!=null && !employeeType.equals(person.getEmployeeType())) {
                needUpdate = true;
                person.setEmployeeType(employeeType);
            }
            if(openId!=null && !openId.equals(person.getIdentificationNumber())) {
                needUpdate = true;
                person.setIdentificationNumber(openId);
            }
            if(socialUser.getAvatar()!=null && !socialUser.getAvatar().equals(person.getAvatar())) {
                needUpdate = true;
                person.setAvatar(socialUser.getAvatar());
            }
            if (needUpdate) {
                sysPersonService.update(person);
            }
        }

        //保存绑定关系
        if(accessAuth == null)
            accessAuth = new OpenAccessAuth();
        accessAuth.setAccessId(socialConfig.getId());
        accessAuth.setPersonId(person.getId());
        accessAuth.setUid(person.getUid());
        accessAuth.setCredential(socialUserId);
        accessAuth.setParams(socialUser.getParams());
        accessAuth.setToken(socialUser.getToken());
        accessAuth.setName(socialUser.getName());
        accessAuth.setRefreshToken(socialUser.getRefreshToken());
        openAccessAuthService.save(accessAuth);

        //系统用户登录，生成token
        AuthLogin login = loadUserAndTokenByUsername(person.getUid(),false);
        return login;
    }

    /**
     * 用户绑定
     * 1. 查询社交平台用户信息
     * 2. 查询绑定的系统用户是否存在
     * 3. 将社交平台用户与系统用户进行绑定
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin bind(AuthLogin authLogin) {

        String socialUserId = authLogin.getId();
        if (ObjectUtils.isEmpty(socialUserId))
            throw new BadRequestAlertException("未传入社交平台用户标识", Entities.AUTH_LOGIN.toString(), "SocialUserId not found");

        String loginName = authLogin.getLoginName();
        String password = authLogin.getPassword();
        if (ObjectUtils.isEmpty(loginName) || ObjectUtils.isEmpty(password))
            throw new BadRequestAlertException("未传入系统用户信息", Entities.AUTH_LOGIN.toString(), "User not found");

        //查询社交用户信息
        OpenAccessAuth socialUser = authenticationService.getSocialUser(socialUserId);
        if (socialUser == null)
            throw new BadRequestAlertException("授权信息已失效，请重新授权", Entities.AUTH_LOGIN.toString(), "Authorization information has expired");

        //使用系统用户登录，查询用户是否存在
        AuthLogin login = login(authLogin);

        //查询社交账号是否已经存在绑定记录 (一个社交账号不能绑定多个系统用户)
        Long socialUserBindCount = openAccessAuthService.count(Wrappers.<OpenAccessAuth>lambdaQuery().eq(OpenAccessAuth::getAccessId, socialUser.getAccessId()).eq(OpenAccessAuth::getCredential, socialUser.getCredential()));
        if (socialUserBindCount > 0)
            throw new BadRequestAlertException(String.format("社交用户[%1$s]存在绑定记录，无法重复绑定", socialUser.getName()), Entities.AUTH_LOGIN.toString(), "User already bound");

        //查询系统用户是否已经被其他社交账号绑定 (一个系统用户不能被多个社交账号绑定)
        AuthInfo person = login.getUser();
        Long userBindCount = openAccessAuthService.count(Wrappers.<OpenAccessAuth>lambdaQuery().eq(OpenAccessAuth::getAccessId, socialUser.getAccessId()).eq(OpenAccessAuth::getPersonId, person.getId()));
        if (userBindCount > 0)
            throw new BadRequestAlertException(String.format("系统用户[%1$s]已经完成绑定，无法重复绑定", person.getUsername()), Entities.AUTH_LOGIN.toString(), "User already bound");

        //将社交用户与系统用户进行绑定
        socialUser.setPersonId(person.getId()).setUid(person.getUsername());
        bindSocialUser(socialUser);
        //清除缓存
        authenticationService.resetSocialUser(socialUserId);
        return login;
    }

    /**
     * 根据社交平台用户与系统用户进行绑定
     * @param socialUser
     * @return
     */
    protected boolean bindSocialUser(OpenAccessAuth socialUser){
        OpenAccessAuth accessAuth = new OpenAccessAuth();
        accessAuth.setAccessId(socialUser.getAccessId());
        accessAuth.setName(socialUser.getName());
        accessAuth.setUid(socialUser.getUid());
        accessAuth.setPersonId(socialUser.getPersonId());
        accessAuth.setCredential(socialUser.getCredential());
        accessAuth.setParams(JSON.toJSONString(socialUser.getParams()));
        return openAccessAuthService.create(accessAuth);
    }

    /**
     * 生成图片验证码
     * @param authLogin
     * @return
     */
    @Override
    public AuthLogin captcha(AuthLogin authLogin) {

        if(captchaService == null)
            throw new BadRequestAlertException("未启用登录验证码配置",Entities.AUTH_LOGIN.toString(),"Verification code configuration not enabled");

        try {
            log.debug("generate captcha verification code");
            String imageCaptchaText = captchaService.createText();
            log.debug("code {}" , imageCaptchaText);
            //过期时间+随机数解决并发情况下生成重复验证码标识问题
            String imageCaptchaId = authenticationService.generateTokenByRSA(imageCaptchaExpiration + DefaultDigitsCreator.getRand().nextInt(99999));
            log.debug("state {}" , imageCaptchaId);
            BufferedImage bufferedImage = captchaService.createImage(imageCaptchaText);
            String base64Image = encodeImage(bufferedImage);
            log.debug("base64Image {}" , base64Image);
            authLogin.setState(imageCaptchaId).setImage(base64Image);
            authenticationService.putImageCaptcha(imageCaptchaId,imageCaptchaText);
        } catch (Exception e) {
            throw new BadRequestAlertException(String.format("生成图片验证码发生异常[%1$s]",e.getMessage()), Entities.AUTH_LOGIN.toString(),"imageCaptcha");
        }
        return authLogin;
    }

    public static String encodeImage(BufferedImage bufferedImage) {
        ByteArrayOutputStream stream = null;
        try {
            stream = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "png", stream);
            String b64Image = "data:image/png;base64," +
                    Base64.getEncoder().encodeToString(stream.toByteArray());

            return b64Image;
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {

                }
            }
        }
        return "";
    }

    @Override
    @Transactional
    public AuthLogin loginBySmsCode(AuthLogin authLogin) {
        String phone = authLogin.getPhone();
        String smsCode = authLogin.getCode();

        if (ObjectUtils.isEmpty(phone))
            throw new BadRequestAlertException("未传入手机号", Entities.AUTH_LOGIN.toString(), "No phone number passed in");

        if (ObjectUtils.isEmpty(smsCode))
            throw new BadRequestAlertException("未传入短信验证码", Entities.AUTH_LOGIN.toString(), "No SMS code passed in");

        String cacheSMSCode = authenticationService.getSMSCode(phone);
        if (ObjectUtils.isEmpty(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码已失效", Entities.AUTH_LOGIN.toString(), "SMS code has expired");

        if (!smsCode.equals(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码不匹配，请重新输入", Entities.AUTH_LOGIN.toString(), "SMS code mismatch");

        //验证用户是否存在
        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getUid, phone).or().eq(SysPerson::getMobile, phone), false);
        if (ObjectUtils.isEmpty(person)){
            //获取默认单位
            SysOrganization defaultOrganization = getDefaultOrganization();
            //不存在则自动创建为新用户
            String employeeNumber = authLogin.get(REGISTER_EMPLOYEE_NUMBER) != null ? authLogin.get(REGISTER_EMPLOYEE_NUMBER).toString() : getDefaultEmployeeNumber();
            person = new SysPerson().setOrganizationId(defaultOrganization.getId()).setOrganizationName(defaultOrganization.getOrganizationName()).setUid(phone).setDisplayName(phone).setMobile(phone).setEmployeeNumber(employeeNumber).setEmployeeType(AuthenticationType.SMS.name().toLowerCase());
            sysPersonService.create(person);
        }
        //系统用户登录，生成token
        AuthLogin login = login(new AuthLogin().setLoginName(person.getUid()).setPassword(person.getUserPassword()).setDc(person.getDc()));
        //清除缓存中的验证码
        authenticationService.resetSMSCode(phone);
        return login;
    }

    /**
     * 重新绑定
     *
     * @param authLogin
     * @return
     */
    @Override
    @Transactional
    public AuthLogin rebind(AuthLogin authLogin) {
        String id = authLogin.getId(); //用户id
        String phone = authLogin.getPhone();
        String smsCode = authLogin.getCode();

        if (ObjectUtils.isEmpty(id) || ObjectUtils.isEmpty(phone) || ObjectUtils.isEmpty(smsCode))
            throw new BadRequestAlertException("传入信息不足，请确认传入信息中是否包含[人员、手机号、短信验证码]", Entities.AUTH_LOGIN.toString(), "user or phone or SMS code cannot be empty");

        if (ObjectUtils.isEmpty(smsCode))
            throw new BadRequestAlertException("验证码不允许为空", Entities.AUTH_LOGIN.toString(), "The verification code cannot be empty");

        String cacheSMSCode = authenticationService.getSMSCode(phone);
        if (ObjectUtils.isEmpty(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码已失效", Entities.AUTH_LOGIN.toString(), "SMS code has expired");

        if (!smsCode.equals(cacheSMSCode))
            throw new BadRequestAlertException("短信验证码不匹配，请重新输入", Entities.AUTH_LOGIN.toString(), "SMS code mismatch");

        SysPerson person = sysPersonService.getOne(Wrappers.<SysPerson>lambdaQuery().eq(SysPerson::getId, id));
        person.setMobile(phone);
        sysPersonService.update(person);

        //换绑手机成功后，清除缓存中的验证码
        if (!ObjectUtils.isEmpty(cacheSMSCode) && !ObjectUtils.isEmpty(phone))
            authenticationService.resetSMSCode(phone);

        return authLogin;
    }

    /**
     * 获取默认单位
     * @return
     */
    protected SysOrganization getDefaultOrganization(){
        SysOrganization defaultOrganization = organizationService.getOne(Wrappers.<SysOrganization>lambdaQuery().select(SysOrganization::getId).isNull(SysOrganization::getParentId).orderByAsc(SysOrganization::getCreateTime,SysOrganization::getSort),false);
        if(defaultOrganization==null || ObjectUtils.isEmpty(defaultOrganization.getId())){
            throw new RuntimeException("创建用户失败，默认单位不存在");
        }
        return defaultOrganization;
    }

}